/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package whisk.core.cli.test

import akka.http.scaladsl.model.StatusCodes.NotFound
import akka.http.scaladsl.model.StatusCodes.OK
import akka.http.scaladsl.model.StatusCodes.BadRequest
import akka.http.scaladsl.model.StatusCodes.Conflict

import java.time.Instant
import java.time.Clock

import scala.language.postfixOps
import scala.concurrent.duration.DurationInt
import scala.util.Random
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import common.TestHelpers
import common.TestUtils
import common.TestUtils._
import common.WhiskProperties
import common.WskProps
import common.WskTestHelpers
import common.rest.WskRest
import spray.json.DefaultJsonProtocol._
import spray.json._
import whisk.core.entity._
import whisk.core.entity.size.SizeInt
import TestJsonArgs._
import whisk.http.Messages

/**
 * Tests for basic CLI usage. Some of these tests require a deployed backend.
 */
@RunWith(classOf[JUnitRunner])
class WskRestBasicUsageTests extends TestHelpers with WskTestHelpers {

  implicit val wskprops: common.WskProps = WskProps()
  val wsk: common.rest.WskRest = new WskRest
  val defaultAction: Some[String] = Some(TestUtils.getTestActionFilename("hello.js"))
  val usrAgentHeaderRegEx: String = """\bUser-Agent\b": \[\s+"OpenWhisk\-CLI/1.\d+.*"""

  behavior of "Wsk CLI usage"

  it should "allow a 3 part Fully Qualified Name (FQN) without a leading '/'" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val guestNamespace = wsk.namespace.whois()
      val packageName = "packageName3ptFQN"
      val actionName = "actionName3ptFQN"
      val triggerName = "triggerName3ptFQN"
      val ruleName = "ruleName3ptFQN"
      val fullQualifiedName = s"${guestNamespace}/${packageName}/${actionName}"
      // Used for action and rule creation below
      assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>
        pkg.create(packageName)
      }
      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
        trigger.create(triggerName)
      }
      // Test action and rule creation where action name is 3 part FQN w/out leading slash
      assetHelper.withCleaner(wsk.action, fullQualifiedName) { (action, _) =>
        action.create(fullQualifiedName, defaultAction)
      }
      assetHelper.withCleaner(wsk.rule, ruleName) { (rule, _) =>
        rule.create(ruleName, trigger = triggerName, action = fullQualifiedName)
      }

      val run = wsk.action.invoke(fullQualifiedName)
      withActivation(wsk.activation, run) { activation =>
        activation.response.status shouldBe "success"
      }
      val action = wsk.action.get(fullQualifiedName)
      action.getField("name") shouldBe actionName
      action.getField("namespace") shouldBe s"${guestNamespace}/${packageName}"
  }

  behavior of "Wsk actions"

  it should "reject creating entities with invalid names" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
    val names = Seq(
      ("", NotFound.intValue),
      (" ", BadRequest.intValue),
      ("hi+there", BadRequest.intValue),
      ("$hola", BadRequest.intValue),
      ("dora?", BadRequest.intValue),
      ("|dora|dora?", BadRequest.intValue))

    names foreach {
      case (name, ec) =>
        assetHelper.withCleaner(wsk.action, name, confirmDelete = false) { (action, _) =>
          action.create(name, defaultAction, expectedExitCode = ec)
        }
    }
  }

  it should "reject action update for sequence with no components" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
    val name = "updateMissingComponents"
    val file = Some(TestUtils.getTestActionFilename("hello.js"))
    assetHelper.withCleaner(wsk.action, name) { (action, name) =>
      action.create(name, file)
    }
    wsk.action
      .create(name, None, update = true, kind = Some("sequence"), expectedExitCode = BadRequest.intValue)
      .stderr should include("The request content was malformed:\n'components' must be defined for sequence kind")
  }

  it should "create, and get an action to verify parameter and annotation parsing" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "actionAnnotations"
      val file = Some(TestUtils.getTestActionFilename("hello.js"))
      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, file, annotations = getValidJSONTestArgInput, parameters = getValidJSONTestArgInput)
      }

      val action = wsk.action.get(name)
      action.getField("name") shouldBe name

      val receivedParams = wsk.parseJsonString(action.stdout).fields("parameters").convertTo[JsArray].elements
      val receivedAnnots = wsk.parseJsonString(action.stdout).fields("annotations").convertTo[JsArray].elements
      val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements

      for (expectedItem <- escapedJSONArr) {
        receivedParams should contain(expectedItem)
        receivedAnnots should contain(expectedItem)
      }
  }

  it should "create, and get an action to verify file parameter and annotation parsing" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "actionAnnotAndParamParsing"
      val file = Some(TestUtils.getTestActionFilename("hello.js"))
      val argInput = Some(TestUtils.getTestActionFilename("validInput1.json"))

      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, file, annotationFile = argInput, parameterFile = argInput)
      }

      val action = wsk.action.get(name)
      action.getField("name") shouldBe name

      val receivedParams = wsk.parseJsonString(action.stdout).fields("parameters").convertTo[JsArray].elements
      val receivedAnnots = wsk.parseJsonString(action.stdout).fields("annotations").convertTo[JsArray].elements
      val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements

      for (expectedItem <- escapedJSONArr) {
        receivedParams should contain(expectedItem)
        receivedAnnots should contain(expectedItem)
      }
  }

  it should "create an action with the proper parameter and annotation escapes" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "actionEscapes"
      val file = Some(TestUtils.getTestActionFilename("hello.js"))

      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, file, parameters = getEscapedJSONTestArgInput, annotations = getEscapedJSONTestArgInput)
      }

      val action = wsk.action.get(name)
      action.getField("name") shouldBe name

      val receivedParams = wsk.parseJsonString(action.stdout).fields("parameters").convertTo[JsArray].elements
      val receivedAnnots = wsk.parseJsonString(action.stdout).fields("annotations").convertTo[JsArray].elements
      val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements

      for (expectedItem <- escapedJSONArr) {
        receivedParams should contain(expectedItem)
        receivedAnnots should contain(expectedItem)
      }
  }

  it should "invoke an action that exits during initialization and get appropriate error" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "abort init"
      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, Some(TestUtils.getTestActionFilename("initexit.js")))
      }

      withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>
        val response = activation.response
        response.result.get.fields("error") shouldBe Messages.abnormalInitialization.toJson
        response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ContainerError)
      }
  }

  it should "invoke an action that hangs during initialization and get appropriate error" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "hang init"
      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, Some(TestUtils.getTestActionFilename("initforever.js")), timeout = Some(3 seconds))
      }

      withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>
        val response = activation.response
        response.result.get.fields("error") shouldBe Messages.timedoutActivation(3 seconds, true).toJson
        response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError)
      }
  }

  it should "invoke an action that exits during run and get appropriate error" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "abort run"
      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, Some(TestUtils.getTestActionFilename("runexit.js")))
      }

      withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>
        val response = activation.response
        response.result.get.fields("error") shouldBe Messages.abnormalRun.toJson
        response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ContainerError)
      }
  }

  it should "ensure keys are not omitted from activation record" in withAssetCleaner(wskprops) {
    val name = "activationRecordTest"

    (wp, assetHelper) =>
      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, Some(TestUtils.getTestActionFilename("argCheck.js")))
      }

      val run = wsk.action.invoke(name)
      withActivation(wsk.activation, run) { activation =>
        activation.start should be > Instant.EPOCH
        activation.end should be > Instant.EPOCH
        activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.Success)
        activation.response.success shouldBe true
        activation.response.result shouldBe Some(JsObject())
        activation.logs shouldBe Some(List())
        activation.annotations shouldBe defined
      }
  }

  it should "write the action-path and the limits to the annotations" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "annotations"
      val memoryLimit = 512 MB
      val logLimit = 1 MB
      val timeLimit = 60 seconds

      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(
          name,
          Some(TestUtils.getTestActionFilename("helloAsync.js")),
          memory = Some(memoryLimit),
          timeout = Some(timeLimit),
          logsize = Some(logLimit))
      }

      val run = wsk.action.invoke(name, Map("payload" -> "this is a test".toJson))
      withActivation(wsk.activation, run) { activation =>
        activation.response.status shouldBe "success"
        val annotations = activation.annotations.get

        val limitsObj = JsObject(
          "key" -> JsString("limits"),
          "value" -> ActionLimits(TimeLimit(timeLimit), MemoryLimit(memoryLimit), LogLimit(logLimit)).toJson)

        val path = annotations.find {
          _.fields("key").convertTo[String] == "path"
        }.get

        path.fields("value").convertTo[String] should fullyMatch regex (s""".*/$name""")
        annotations should contain(limitsObj)
      }
  }

  it should "report error when creating an action with unknown kind" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val rr = assetHelper.withCleaner(wsk.action, "invalid kind", confirmDelete = false) { (action, name) =>
        action.create(
          name,
          Some(TestUtils.getTestActionFilename("echo.js")),
          kind = Some("foobar"),
          expectedExitCode = BadRequest.intValue)
      }
      rr.stderr should include("kind 'foobar' not in Set")
  }

  it should "report error when creating an action with zip but without kind" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "zipWithNoKind"
      val zippedPythonAction = Some(TestUtils.getTestActionFilename("python.zip"))
      val createResult = assetHelper.withCleaner(wsk.action, name, confirmDelete = false) { (action, _) =>
        action.create(name, zippedPythonAction, expectedExitCode = ANY_ERROR_EXIT)
      }

      createResult.stderr should include("kind '' not in Set")
  }

  it should "create, and invoke an action that utilizes an invalid docker container with appropriate error" in withAssetCleaner(
    wskprops) {
    val name = "invalidDockerContainer"
    val containerName = s"bogus${Random.alphanumeric.take(16).mkString.toLowerCase}"

    (wp, assetHelper) =>
      assetHelper.withCleaner(wsk.action, name) {
        // docker name is a randomly generate string
        (action, _) =>
          action.create(name, None, docker = Some(containerName))
      }

      val run = wsk.action.invoke(name)
      withActivation(wsk.activation, run) { activation =>
        activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError)
        activation.response.result.get
          .fields("error") shouldBe s"Failed to pull container image '$containerName'.".toJson
        activation.annotations shouldBe defined
        val limits = activation.annotations.get.filter(_.fields("key").convertTo[String] == "limits")
        withClue(limits) {
          limits.length should be > 0
          limits(0).fields("value") should not be JsNull
        }
      }
  }

  it should "invoke an action using npm openwhisk" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
    val name = "hello npm openwhisk"
    assetHelper.withCleaner(wsk.action, name, confirmDelete = false) { (action, _) =>
      action.create(name, Some(TestUtils.getTestActionFilename("helloOpenwhiskPackage.js")))
    }

    val run = wsk.action.invoke(name, Map("ignore_certs" -> true.toJson, "name" -> name.toJson))
    withActivation(wsk.activation, run) { activation =>
      activation.response.status shouldBe "success"
      activation.response.result shouldBe Some(JsObject("delete" -> true.toJson))
      activation.logs.get.mkString(" ") should include("action list has this many actions")
    }

    wsk.action.delete(name, expectedExitCode = NotFound.intValue)
  }

  it should "invoke an action receiving context properties" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
    val namespace = wsk.namespace.whois()
    val name = "context"
    assetHelper.withCleaner(wsk.action, name) { (action, _) =>
      action.create(name, Some(TestUtils.getTestActionFilename("helloContext.js")))
    }

    val start = Instant.now(Clock.systemUTC()).toEpochMilli
    val run = wsk.action.invoke(name)
    withActivation(wsk.activation, run) { activation =>
      activation.response.status shouldBe "success"
      val fields = activation.response.result.get.convertTo[Map[String, String]]
      fields("api_host") shouldBe WhiskProperties.getApiHostForAction
      fields("api_key") shouldBe wskprops.authKey
      fields("namespace") shouldBe namespace
      fields("action_name") shouldBe s"/$namespace/$name"
      fields("activation_id") shouldBe activation.activationId
      fields("deadline").toLong should be >= start
    }
  }

  it should "invoke an action successfully with options --blocking and --result" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "invokeResult"
      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, Some(TestUtils.getTestActionFilename("echo.js")))
      }
      val args = Map("hello" -> "Robert".toJson)
      val run = wsk.action.invoke(name, args, blocking = true, result = true)
      run.stdout.parseJson shouldBe args.toJson
  }

  it should "invoke an action that returns a result by the deadline" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "deadline"
      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, Some(TestUtils.getTestActionFilename("helloDeadline.js")), timeout = Some(3 seconds))
      }

      val run = wsk.action.invoke(name)
      withActivation(wsk.activation, run) { activation =>
        activation.response.status shouldBe "success"
        activation.response.result shouldBe Some(JsObject("timedout" -> true.toJson))
      }
  }

  it should "invoke an action twice, where the first times out but the second does not and should succeed" in withAssetCleaner(
    wskprops) {
    // this test issues two activations: the first is forced to time out and not return a result by its deadline (ie it does not resolve
    // its promise). The invoker should reclaim its container so that a second activation of the same action (which must happen within a
    // short period of time (seconds, not minutes) is allocated a fresh container and hence runs as expected (vs. hitting in the container
    // cache and reusing a bad container).
    (wp, assetHelper) =>
      val name = "timeout"
      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, Some(TestUtils.getTestActionFilename("helloDeadline.js")), timeout = Some(3 seconds))
      }

      val start = Instant.now(Clock.systemUTC()).toEpochMilli
      val hungRun = wsk.action.invoke(name, Map("forceHang" -> true.toJson))
      withActivation(wsk.activation, hungRun) { activation =>
        // the first action must fail with a timeout error
        activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError)
        activation.response.result shouldBe Some(
          JsObject("error" -> Messages.timedoutActivation(3 seconds, false).toJson))
      }

      // run the action again, this time without forcing it to timeout
      // it should succeed because it ran in a fresh container
      val goodRun = wsk.action.invoke(name, Map("forceHang" -> false.toJson))
      withActivation(wsk.activation, goodRun) { activation =>
        // the first action must fail with a timeout error
        activation.response.status shouldBe "success"
        activation.response.result shouldBe Some(JsObject("timedout" -> true.toJson))
      }
  }

  it should "ensure --web flags set the proper annotations" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
    Seq("true", "faLse", "tRue", "nO", "yEs", "no", "raw", "NO", "Raw").foreach { flag =>
      val webEnabled = flag.toLowerCase == "true" || flag.toLowerCase == "yes"
      val rawEnabled = flag.toLowerCase == "raw"

      val name = "webaction-" + flag
      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, Some(TestUtils.getTestActionFilename("echo.js")), web = Some(flag.toLowerCase))
      }

      val action = wsk.action.get(name)
      action.getFieldJsValue("annotations").convertTo[Set[JsObject]] shouldBe Set(
        JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6")),
        JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(webEnabled || rawEnabled)),
        JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(rawEnabled)),
        JsObject("key" -> JsString("final"), "value" -> JsBoolean(webEnabled || rawEnabled)))
    }
  }

  it should "ensure action update with --web flag only copies existing annotations when new annotations are not provided" in withAssetCleaner(
    wskprops) { (wp, assetHelper) =>
    val name = "webaction"
    val file = Some(TestUtils.getTestActionFilename("echo.js"))
    val createKey = "createKey"
    val createValue = JsString("createValue")
    val updateKey = "updateKey"
    val updateValue = JsString("updateValue")
    val origKey = "origKey"
    val origValue = JsString("origValue")
    val overwrittenValue = JsString("overwrittenValue")
    val createAnnots = Map(createKey -> createValue, origKey -> origValue)
    val updateAnnots = Map(updateKey -> updateValue, origKey -> overwrittenValue)

    assetHelper.withCleaner(wsk.action, name) { (action, _) =>
      action.create(name, file, annotations = createAnnots)
    }

    wsk.action.create(name, file, web = Some("true"), update = true)

    val action = wsk.action.get(name)
    action.getFieldJsValue("annotations") shouldBe JsArray(
      JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(true)),
      JsObject("key" -> JsString(origKey), "value" -> origValue),
      JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(false)),
      JsObject("key" -> JsString("final"), "value" -> JsBoolean(true)),
      JsObject("key" -> JsString(createKey), "value" -> createValue),
      JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6")))

    wsk.action.create(name, file, web = Some("true"), update = true, annotations = updateAnnots)

    val updatedAction = wsk.action.get(name)
    updatedAction.getFieldJsValue("annotations") shouldBe JsArray(
      JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(true)),
      JsObject("key" -> JsString(origKey), "value" -> overwrittenValue),
      JsObject("key" -> JsString(updateKey), "value" -> updateValue),
      JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(false)),
      JsObject("key" -> JsString("final"), "value" -> JsBoolean(true)),
      JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6")))
  }

  it should "ensure action update creates an action with --web flag" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "webaction"
      val file = Some(TestUtils.getTestActionFilename("echo.js"))

      assetHelper.withCleaner(wsk.action, name) { (action, _) =>
        action.create(name, file, web = Some("true"), update = true)
      }

      val action = wsk.action.get(name)
      action.getFieldJsValue("annotations") shouldBe JsArray(
        JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(true)),
        JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(false)),
        JsObject("key" -> JsString("final"), "value" -> JsBoolean(true)),
        JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6")))
  }

  it should "invoke action while not encoding &, <, > characters" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
    val name = "nonescape"
    val file = Some(TestUtils.getTestActionFilename("hello.js"))
    val nonescape = "&<>"
    val input = Map("payload" -> nonescape.toJson)
    val output = JsObject("payload" -> JsString(s"hello, $nonescape!"))

    assetHelper.withCleaner(wsk.action, name) { (action, _) =>
      action.create(name, file)
    }

    withActivation(wsk.activation, wsk.action.invoke(name, parameters = input)) { activation =>
      activation.response.success shouldBe true
      activation.response.result shouldBe Some(output)
      activation.logs.toList.flatten.filter(_.contains(nonescape)).length shouldBe 1
    }
  }

  behavior of "Wsk packages"

  it should "create, and delete a package" in {
    val name = "createDeletePackage"
    wsk.pkg.create(name).statusCode shouldBe OK
    wsk.pkg.delete(name).statusCode shouldBe OK
  }

  it should "create, and get a package to verify parameter and annotation parsing" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "packageAnnotAndParamParsing"

      assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>
        pkg.create(name, annotations = getValidJSONTestArgInput, parameters = getValidJSONTestArgInput)
      }

      val pack = wsk.pkg.get(name)

      val receivedParams = wsk.parseJsonString(pack.stdout).fields("parameters").convertTo[JsArray].elements
      val receivedAnnots = wsk.parseJsonString(pack.stdout).fields("annotations").convertTo[JsArray].elements
      val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements

      for (expectedItem <- escapedJSONArr) {
        receivedParams should contain(expectedItem)
        receivedAnnots should contain(expectedItem)
      }
  }

  it should "create, and get a package to verify file parameter and annotation parsing" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "packageAnnotAndParamFileParsing"
      val file = Some(TestUtils.getTestActionFilename("hello.js"))
      val argInput = Some(TestUtils.getTestActionFilename("validInput1.json"))

      assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>
        pkg.create(name, annotationFile = argInput, parameterFile = argInput)
      }

      val stdout = wsk.pkg.get(name).stdout
      val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
      val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
      val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements

      for (expectedItem <- escapedJSONArr) {
        receivedParams should contain(expectedItem)
        receivedAnnots should contain(expectedItem)
      }
  }

  it should "create a package with the proper parameter and annotation escapes" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "packageEscapses"

      assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>
        pkg.create(name, parameters = getEscapedJSONTestArgInput, annotations = getEscapedJSONTestArgInput)
      }

      val pack = wsk.pkg.get(name)
      val receivedParams = wsk.parseJsonString(pack.stdout).fields("parameters").convertTo[JsArray].elements
      val receivedAnnots = wsk.parseJsonString(pack.stdout).fields("annotations").convertTo[JsArray].elements
      val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements

      for (expectedItem <- escapedJSONArr) {
        receivedParams should contain(expectedItem)
        receivedAnnots should contain(expectedItem)
      }
  }

  it should "report conformance error accessing action as package" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
    val name = "aAsP"
    val file = Some(TestUtils.getTestActionFilename("hello.js"))
    assetHelper.withCleaner(wsk.action, name) { (action, _) =>
      action.create(name, file)
    }

    wsk.pkg.get(name, expectedExitCode = Conflict.intValue).stderr should include(Messages.conformanceMessage)

    wsk.pkg.bind(name, "bogus", expectedExitCode = Conflict.intValue).stderr should include(
      Messages.requestedBindingIsNotValid)

    wsk.pkg.bind("bogus", "alsobogus", expectedExitCode = BadRequest.intValue).stderr should include(
      Messages.bindingDoesNotExist)

  }

  behavior of "Wsk triggers"

  it should "create, and get a trigger to verify parameter and annotation parsing" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "triggerAnnotAndParamParsing"

      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
        trigger.create(name, annotations = getValidJSONTestArgInput, parameters = getValidJSONTestArgInput)
      }

      val stdout = wsk.trigger.get(name).stdout

      val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
      val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
      val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements

      for (expectedItem <- escapedJSONArr) {
        receivedParams should contain(expectedItem)
        receivedAnnots should contain(expectedItem)
      }
  }

  it should "create, and get a trigger to verify file parameter and annotation parsing" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "triggerAnnotAndParamFileParsing"
      val file = Some(TestUtils.getTestActionFilename("hello.js"))
      val argInput = Some(TestUtils.getTestActionFilename("validInput1.json"))

      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
        trigger.create(name, annotationFile = argInput, parameterFile = argInput)
      }

      val stdout = wsk.trigger.get(name).stdout

      val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
      val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
      val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements

      for (expectedItem <- escapedJSONArr) {
        receivedParams should contain(expectedItem)
        receivedAnnots should contain(expectedItem)
      }
  }

  it should "create a trigger with the proper parameter and annotation escapes" in withAssetCleaner(wskprops) {
    (wp, assetHelper) =>
      val name = "triggerEscapes"

      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
        trigger.create(name, parameters = getEscapedJSONTestArgInput, annotations = getEscapedJSONTestArgInput)
      }

      val stdout = wsk.trigger.get(name).stdout

      val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
      val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
      val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements

      for (expectedItem <- escapedJSONArr) {
        receivedParams should contain(expectedItem)
        receivedAnnots should contain(expectedItem)
      }
  }

  it should "not create a trigger when feed fails to initialize" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
    assetHelper.withCleaner(wsk.trigger, "badfeed", confirmDelete = false) { (trigger, name) =>
      trigger.create(name, feed = Some(s"bogus"), expectedExitCode = ANY_ERROR_EXIT).exitCode should equal(NOT_FOUND)
      trigger.get(name, expectedExitCode = NotFound.intValue)

      trigger.create(name, feed = Some(s"bogus/feed"), expectedExitCode = ANY_ERROR_EXIT).exitCode should equal(
        NOT_FOUND)
      trigger.get(name, expectedExitCode = NotFound.intValue)
    }
  }

  it should "invoke a feed action with the correct lifecyle event when creating, retrieving and deleting a feed trigger" in withAssetCleaner(
    wskprops) { (wp, assetHelper) =>
    val actionName = "echo"
    val triggerName = "feedTest"

    assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>
      action.create(actionName, Some(TestUtils.getTestActionFilename("echo.js")))
    }

    try {
      wsk.trigger.create(triggerName, feed = Some(actionName)).statusCode shouldBe OK

      wsk.trigger.get(triggerName).statusCode shouldBe OK
    } finally {
      wsk.trigger.delete(triggerName).statusCode shouldBe OK
    }
  }
}
